【LIFF】LINEのユーザー情報をサーバーサイドで使用する際のアンチパターンと適切な実装方法のご紹介
リテールアプリ共創部のるおんです。
先日LIFFアプリの開発において、必要なLINEのユーザー情報をデータベースに保存する機能を開発する機会がありました。
LIFFアプリを開発する際、ユーザー情報の取り扱いは非常に重要なポイントです。今回は、LINEのユーザー情報をサーバーで使用する際のアンチパターンと、その正しい対応方法について解説します。
ユースケース
LIFFアプリを開発する中で、以下のようなユースケースがよくあります:
- ユーザーのプロフィール情報をアプリ内で表示する
- ユーザーの基本情報をデータベースに保存し、アプリの機能をパーソナライズする
- ユーザーの識別情報を使用して、アプリ内での活動を追跡する
これらのユースケースを実現するために、LINEのユーザー情報を取得し、サーバーサイドで処理する必要があります。例えば、ユーザーがアプリを初めて使用する際にその人のLINEプロフィール情報(名前、プロフィール画像など)を取得し、アプリのデータベースに保存したいという要件がよくあります。
他にも、取得したユーザーをカテゴライズしてUIDを用いて特定のユーザー群にのみLINEメッセージを一斉配信したいなどの要件もあるかもしれません。
ユーザー情報の取得方法
そもそも、LIFFアプリを開発するにあたってユーザー情報を取得するには大きく分けて2パターンがあります。
- クライアントサイドで LIFF API のliff.getProfile()メソッドを用いて取得
- サーバーサイドで LINEログイン API を用いて取得
しかし、これらのAPIを用いて取得したユーザー情報は適切に取り扱う必要があります。
ユーザー情報の取扱方法
例えば、LIFFアプリを使用したユーザーのデータをデータベースに保存したいと思います。
以下はアンチパターンです。
アンチパターン
- クライアントサイドでLIFFアプリを構築し、
liff.getProfile
メソッドを用いて取得したユーザー情報を取得します - 取得したユーザー情報をサーバーサイドにリクエスト送信します。
- サーバーサイドでクライアントから渡ってきたユーザー情報を処理してDBに保存します。
このような実装はセキュリティの観点から問題があります。
正しいアプローチ
代わりに、以下のような方法を採用すべきです:
- クライアントサイドでは、LIFFのIDトークン、またはアクセストークンのみを取得します。
- このIDトークン(or アクセストークン)をサーバーサイドに送信します。
- クライアントサイドから渡ってきたIDトークン(or アクセストークン)を用いて、サーバーサイドでLINEログインAPIを使用してユーザー情報を取得します。
- 取得したユーザー情報をデータベースに保存します。
LINEの公式ドキュメントにもユーザー情報を適切に扱うためのシーケンス図がわかりやすく載っています。
IDトークンの場合
アクセストークンの場合
なぜユーザー情報の取り扱いに注意が必要か
LIFFアプリでユーザー情報を扱う際、セキュリティとプライバシーの観点から、適切な方法で情報を取得し、サーバーに送信することが重要です。LINEの公式ドキュメントでも、以下のように注意喚起されています:
ユーザー情報をサーバーに送信しないでください
liff.getDecodedIDToken()およびliff.getProfile()で取得したユーザーのプロフィールの詳細を、LIFFアプリからサーバーに送信しないでください。
LIFFアプリで、これらのユーザー情報を正しく処理しないと、なりすましやその他の種類の攻撃に対して脆弱になります。
実際にユーザー情報を正しく取得してみた
全体像
実装する全体像は以下のとおりです。今回はアクセストークンを使用してユーザー情報を取得したいと思います。
- クライアントサイドではReactを用いてLIFFアプリを作成します。
- サーバーサイドにはLambda関数でNode.jsを実行します。
- データベースにDynamoDBを使用してユーザー情報を保存します。。
では早速実装していきたいと思います。
必要なインフラリソースはAWS CDKでサクッと作っちゃいます。
user-profile-cdk-stack
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as aws_dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { RemovalPolicy } from 'aws-cdk-lib';
export class UserProfileCdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
/**
* API Gatewayで使用するLambda
*/
const lineUserProfileLambda = new nodejs.NodejsFunction(this, "lineUserProfileLambda", {
entry: "server/handler/lineUserProfile.ts",
runtime: lambda.Runtime.NODEJS_20_X,
functionName: "lineUserProfileLambda",
description: "サンプルのラムダ関数を作成",
architecture: lambda.Architecture.ARM_64,
});
/**
* API Gateway
*/
const api = new apigateway.LambdaRestApi(this, "lineUserProfileApi", {
handler: lineUserProfileLambda,
proxy: false,
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS,
allowMethods: apigateway.Cors.ALL_METHODS,
allowHeaders: apigateway.Cors.DEFAULT_HEADERS,
statusCode: 200,
},
});
const lineUserProfileIntegration = new apigateway.LambdaIntegration(lineUserProfileLambda);
api.root.addMethod("POST", lineUserProfileIntegration)
/**
* DynamoDB
*/
const lineUserProfileTable = new aws_dynamodb.Table(
this,
"LineUserProfileTable",
{
partitionKey: {
name: "id",
type: aws_dynamodb.AttributeType.STRING,
},
billingMode: aws_dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: RemovalPolicy.RETAIN,
tableName: "LineUserProfile",
pointInTimeRecovery: true,
},
);
lineUserProfileTable.grantReadWriteData(lineUserProfileLambda);
}
}
クライアントサイド(React)
create-liif-app
で簡単にLIFFアプリを構築し、App.tsxに以下のコードを記述しました。
import { useEffect, useState } from "react";
import liff from "@line/liff";
import "./App.css";
import axios from "axios";
export const LIFFApp = () => {
useEffect(() => {
liff.init({
liffId: import.meta.env.VITE_LIFF_ID,
});
});
const handleSubmit = async () => {
+ const accessToken = await liff.getAccessToken();
+ if (accessToken) {
+ const res = await axios.post(
+ "https://hogehoge", // サーバーサイドへのリクエストエンドポイント
+ { accessToken }
+ );
}
};
return (
<div className="App">
<h1>LIFF APP</h1>
<div>
<div className="button-container">
<p>ユーザー情報を送信する</p>
<button onClick={handleSubmit}>送信</button>
</div>
</div>
</div>
);
};
CSS
.App {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.button-container {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 20px;
}
button {
width: 100px;
height: 50px;
font-size: 16px;
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
cursor: pointer;
border-radius: 5px;
}
ハイライトしているところが重要な点です。今回の実装ではアクセストークンをLIFF APIを用いて取得し、それをサーバーサイドに送信しています。
フロントエンドでは特にユーザー情報を取得していないところが重要ですね。(フロントで完結するユーザー情報なら取得して使用しても良い)
以下はアンチパターン
❌ダメな例
const handleSubmit = async () => {
// ユーザー情報を直接サーバーサイドに送信している
const userData = await liff.getProfile();
axios.post(
"https://hogehoge",
{
userData: userData,
}
);
};
サーバーサイド (Lambda関数)
import axios from "axios";
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
const dynamoDB = new DynamoDBClient({ region: 'ap-northeast-1' });
const dynamoDBDocumentClient = DynamoDBDocumentClient.from(dynamoDB);
export const handler = async (event: any) => {
console.log(event);
const { accessToken } = JSON.parse(event.body);
try {
// アクセストークンの検証
+ const verifyResponse = await axios.get("https://api.line.me/oauth2/v2.1/verify", {
+ params: { access_token: accessToken }
+ });
// 検証が成功した場合、ユーザー情報を取得
if (verifyResponse.status === 200) {
+ const userInfoResponse = await axios.get("https://api.line.me/v2/profile", {
+ headers: {
+ Authorization: `Bearer ${accessToken}`
+ }
});
// ここで、DynamoDBにユーザー情報を保存
await dynamoDBDocumentClient.send(new PutCommand({
TableName: 'LineUserProfile',
Item: {
id: userInfoResponse.data.userId,
lineDisplayName: userInfoResponse.data.displayName,
pictureUrl: userInfoResponse.data.pictureUrl,
statusMessage: userInfoResponse.data.statusMessage
}
}));
return {
statusCode: 200,
body: JSON.stringify({ message: "ユーザー情報を保存しました" })
};
} else {
throw new Error("アクセストークンの検証に失敗しました");
}
} catch (error) {
console.error("エラー:", error);
return {
statusCode: 400,
body: JSON.stringify({ error: "無効なアクセストークン" })
};
}
};
サーバーサイドでは、受け取ったアクセストークンを検証し、LINEプラットフォームからユーザー情報を安全に取得しています。取得したユーザー情報はDynamoDBに保存されます。
アクセストークンの有効性を検証
GET https://api.line.me/oauth2/v2.1/verify
ユーザープロフィールを取得する
GET https://api.line.me/v2/profile
このような構成にすることで、サーバーサイドで安全にユーザー情報を扱うことができます!
動作確認
LIFFアプリから送信ボタンを押すと、無事DynamoDBに値が保存されていることが確認できます。
おわりに
今回はLIFFアプリでユーザー情報を扱う際のアンチパターンと適切な実装方法をご紹介しました。LIFFアプリでユーザー情報を扱う際は、セキュリティとプライバシーを最優先に考える必要があります。以下が重要な点です。
- クライアントサイドでは最小限の情報(トークン)のみを扱い、ユーザーの詳細情報は直接取得しない。
- サーバーサイドでトークンを検証し、LINEプラットフォームから安全にユーザー情報を取得する。
- 取得したユーザー情報を適切に処理し、必要な情報のみをデータベースに保存する。
基本的にはドキュメントに書いてある内容ですが、実際にコードの実装例を提示しながら解説してみました。
以上。どなたかの参考になれば幸いです。
参考